</> 技術筆記Tech Notes

JWT 驗證機制詳解:Access Token 與 Refresh Token 實作指南

在 Web 專案的開發中,身分驗證(Authentication)與狀態管理是不可或缺的環節。早期常使用基於伺服器記憶體的 Session 機制,但隨著系統微服務化與前後端分離架構的普及,JWT (JSON Web Token) 因為其「無狀態(Stateless)」的特性,成為了目前的主流方案。

然而,純粹的 JWT 機制存在著安全與管理上的缺陷。為了兼顧「系統安全性」與「良好的使用者體驗」,業界通常會採用 Access Token 搭配 Refresh Token 的雙令牌(Dual Token)機制。本篇將詳細說明其運作原理與實作細節。

1. 什麼是 JWT?

JWT 是一種開放標準(RFC 7519),主要用來在各方之間以 JSON 物件的形式安全地傳遞資訊。它的核心優勢在於自包含(Self-contained),這意味著 Token 本身就包含了使用者的基本資訊與驗證所需的簽章,後端伺服器在收到 Token 時,不需要頻繁查詢資料庫就能確認該使用者的身分與權限。

JWT 的資料結構

JWT 由三個部分組成,以點號(.)分隔:Header.Payload.Signature

  1. Header(標頭)

    記錄 Token 的類型(通常是 JWT)以及使用的雜湊加密演算法(例如 HMAC SHA256 或 RSA)。

  2. Payload(負載)

    存放實際要傳遞的資料(稱為 Claims),例如 user_idrole(權限角色),以及最重要的 exp(過期時間)。

    • ⚠️ 重要觀念:Payload 只是經過 Base64Url 編碼,並沒有加密。前端任何人都可以輕易解碼看到內容,因此絕對不可以把密碼、身分證字號等敏感個資放在 Payload 裡面。

  3. Signature(簽章)

    由後端伺服器保管的「私鑰(Secret Key)」加上 Header 與 Payload 組合後進行雜湊運算產生。如果有人竄改了 Payload 的內容,後端在驗證時會發現簽章對不起來,從而拒絕該請求。

2. 為什麼需要雙令牌(Dual Token)機制?

如果我們的系統只發放單一的 Access Token,在設定過期時間(exp)時會面臨兩難:

  • 情境一:設定長效期(例如 30 天)

    • 優點:使用者體驗好,不用頻繁登入。

    • 致命缺點:一旦 Token 被駭客竊取(例如透過 XSS 攻擊),駭客在接下來的 30 天內都能假冒該使用者。因為 JWT 是無狀態的,後端在不大幅修改架構的前提下,很難單獨把某個發送出去的 Token 宣告作廢。

  • 情境二:設定短效期(例如 15 分鐘)

    • 優點:安全性高。就算 Token 被偷,駭客能利用的時間也很短。

    • 致命缺點:使用者體驗極差。每隔 15 分鐘,正在操作系統的使用者就會被強制登出,要求重新輸入帳號密碼。

解決方案:Access Token + Refresh Token

為了在安全性與體驗之間取得平衡,我們將權限拆分成兩把鑰匙:

  1. Access Token(存取令牌)

    • 效期短(通常為 5 到 15 分鐘)。

    • 權限高,每次呼叫需要授權的 API 時都必須放在 HTTP Header 的 Authorization: Bearer <token> 中帶上。

  2. Refresh Token(更新令牌)

    • 效期長(通常為 7 天到 30 天)。

    • 權限單一,唯一的作用就是拿去跟伺服器換取「新的 Access Token」。

    • 平常不參與 API 請求傳輸,降低被攔截的風險。

3. 核心運作流程與時序圖

在實際的單頁式應用(SPA,如 React、Vue)中,雙令牌機制的運作通常會依賴前端的 HTTP 攔截器(Interceptor,例如 axios.interceptors) 來達成「無感刷新」。

詳細流程如下:

sequenceDiagram
    autonumber
    participant User as 前端應用 (Browser)
    participant API as 後端 API 伺服器
    participant Auth as 授權伺服器 (Auth Service)
    participant DB as 資料庫 / Redis

    Note over User, DB: --- 階段一:登入取得 Token ---
    User->>Auth: POST /login (提交帳號密碼)
    Auth->>DB: 驗證帳密
    DB-->>Auth: 驗證成功
    Auth-->>User: 回傳 Access Token (短期) <br/>並透過 Set-Cookie 寫入 Refresh Token (長期)

    Note over User, API: --- 階段二:正常 API 請求 ---
    User->>API: 攜帶 Access Token 請求資料 (GET /data)
    API-->>User: 200 OK (驗證成功,回傳資料)

    Note over User, API: --- 階段三:Token 過期與攔截刷新 ---
    User->>API: 攜帶 Access Token 請求資料 (GET /data)
    API-->>User: 401 Unauthorized (Access Token 已過期)
    
    rect rgb(245, 245, 245)
        Note right of User: 前端攔截器捕捉到 401 錯誤,暫停原請求
        User->>Auth: POST /refresh (自動攜帶 Cookie 中的 Refresh Token)
        Auth->>DB: 檢查 Refresh Token 是否有效且未被列入黑名單
        DB-->>Auth: 狀態正常
        Auth-->>User: 核發新的 Access Token
    end

    Note over User, API: --- 階段四:自動重試 ---
    User->>API: 攔截器使用「新的 Access Token」重新發送原先失敗的請求
    API-->>User: 200 OK (使用者完全沒有察覺 Token 曾經過期)

實作細節:併發請求問題 (Race Condition)

當頁面載入時,前端可能會同時發出 5 支 API 請求。如果此時 Access Token 剛好過期,這 5 支 API 都會回傳 401。

一個健壯的前端攔截器必須實作「鎖(Lock)」或「等待佇列(Queue)」機制:

  • 第一支收到 401 的請求會觸發 /refresh 動作,並把一個旗標(如 isRefreshing = true)立起來。

  • 剩下四支收到 401 的請求,會被暫存到一個 Queue 陣列中等待。

  • 等到 /refresh 成功拿到新 Token 後,再一次把 Queue 裡面的這四個請求用新 Token 重發出去。這能避免對後端造成不必要的重複刷新壓力。

4. 系統安全性最佳實踐 (Best Practices)

在實作雙令牌機制時,Token 的儲存位置與生命週期管理是資安防護的重點。

A. 儲存策略:防範 XSS 與 CSRF

  • Access Token

    • 建議儲存位置:前端的記憶體變數(Memory Variable),例如 React 的 State 或是閉包(Closure)中。

    • 原因:放在記憶體中,駭客的 XSS 腳本很難直接去讀取。缺點是使用者只要按 F5 重新整理頁面,Token 就會消失。但沒關係,因為我們有 Refresh Token,頁面載入時立刻打一次 /refresh 拿回 Access Token 即可。

    • 不建議localStorage。因為 localStorage 很容易被惡意的第三方 JavaScript 套件讀取(即 XSS 攻擊)。

  • Refresh Token

    • 建議儲存位置HttpOnly Cookie

    • 原因:加上 HttpOnly 屬性後,瀏覽器會嚴格禁止任何 JavaScript 讀取這個 Cookie,從根本上阻斷了 XSS 竊取 Refresh Token 的可能。

    • 防禦配套:由於使用 Cookie 會有 CSRF(跨站請求偽造)的風險,發送 Refresh Token 的 Cookie 必須設定 Secure(僅限 HTTPS 傳輸)與 SameSite=StrictLax,並且在後端加上 CORS 限制。

B. 令牌輪轉機制 (Refresh Token Rotation)

為了進一步提升安全性,目前主流建議採用「輪轉機制」。

傳統做法是 Refresh Token 一直用到過期為止;輪轉機制則是:每次前端拿 Refresh Token 去換新的 Access Token 時,後端除了給新的 Access Token,同時也會發配一把「全新的 Refresh Token」,並把舊的 Refresh Token 標記為失效。

  • 安全效益:如果使用者的舊 Refresh Token 被駭客偷走,當駭客嘗試拿去後端換票時,後端會發現「這個舊 Token 明明已經被換過一次了,怎麼又有人拿來換?」。此時系統可以判定該帳號可能遭到入侵,從而觸發安全機制,立刻撤銷該使用者名下的所有 Token,強制雙方都必須重新輸入帳號密碼登入。

C. 黑名單與強制登出 (Revocation)

雖然 Access Token 是無狀態的,但我們必須將 Refresh Token 有狀態化(存入關聯式資料庫或 Redis 中)。

當發生以下情況時,後端應將對應的 Refresh Token 在資料庫中標記為「撤銷(Revoked)」:

  1. 使用者主動點擊「登出」。

  2. 使用者更改密碼。

  3. 管理員在後台強制將某個使用者踢下線。

    一旦 Refresh Token 被撤銷,前端攔截器的自動換票就會失敗,使用者就會被順理成章地導回登入頁面,確保了系統的管理控制權。

5. 總結

導入 Access Token 與 Refresh Token 的機制,雖然增加了前後端開發的複雜度(尤其是前端攔截器的狀態管理,以及後端資料庫對 Refresh Token 的控管),但這是目前在 Web 開發領域中,少數能同時兼顧「後端系統擴展性(無狀態)」、「高度安全性(降低權限外洩風險)」以及「優良使用者體驗(無感續航)」的成熟架構方案。